#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import sys
import argparse
import subprocess

import ctools.common.mycmd as mycmd
from ctools.common.strhelper import *
from ctools.common.ifilter import *

import ctools.datahelper.cscopehelper as cscopehelper
from ctools.datahelper.functree import (SNode,)

from ctools.ly_parser.nodes import *
import ctools.ly_parser.func_parser as func_parser

from ctools.converters.conv_ln import ConvLinkN2C
from ctools.converters.conv_ln import ConvLinkN2Gv

from ctools.subcmd.ctbase import CToolsCmd

import logging
from ctools.logext import *

_rlog, _plog, _mlog, _tlog = get_loggers(__file__, __name__, propagate=False)

_fpath = os.path.dirname(os.path.realpath(__file__))
_rcs_path = os.path.join(_fpath, '../rcs')

_prompt = 'CTree=> '

class CTreeCli(mycmd.MyCmd):
    def __init__(self, db, mode='c', ctrl_direction='',
                prompt=_prompt,
                tryshell=False, autorep=True,
                comment_prefix='#',
                **kwargs):
        super().__init__(prompt=prompt,
                        tryshell=tryshell, autorep=autorep,
                        comment_prefix=comment_prefix)
        params = {
            'cscope_db':        db,
            'mode':             mode,
            'ctrl_direction':   ctrl_direction,
            'sequence':         True,
            'unique':           True,
        }
        param_helps = {
            'cscope_db':       'cscope database file path',
            'mode':            'output format: c/gvtext/calltree/tree',
            'ctrl_direction':  'control direction of call tree: >/</auto',
            'sequence':        'generate {rank=same ...}',
            'unique':          'remove duplicate nodes',
        }
        self._params.update(params)
        self._param_helps.update(param_helps)

        if kwargs:
            self._params.update(kwargs)

        self._func_parser = func_parser.GVCFuncParser()

        ifilter = iFilter()
        ignore_txt = os.path.join(_rcs_path, 'ignore.txt')
        if os.path.exists(ignore_txt):
            ifilter.loadtxt(ignore_txt, is_ignore=True)

        self._tree_prov = cscopehelper.CScopeTree(db)
        self._tree_prov.set_filter(ifilter)

        self._conv_ln2c = ConvLinkN2C(self._tree_prov)
        self._conv_ln2gv = ConvLinkN2Gv(self._tree_prov)

        self.err_style = Styler(F_RED)

    def _check_param_db(self):
        return os.path.exists(self.param_value('cscope_db'))

    def _func_parse_code(self, code):
        filename = 'cli'
        funcname = 'main'
        self._func_parser.start_parse(funcname)
        return self._func_parser.parse_text(code, filename)

    def show_tnode_gvtext(self, nd_main):
        ctrl_direction = self.param_value('ctrl_direction')
        sequence = self.param_yes('sequence')
        unique = self.param_yes('unique')

        text_list = []
        for nd in nd_main:
            if not isinstance(nd, LinkNode):
                _plog.warning(f'not a LinkNode {nd.name}')
                continue
            text = self._conv_ln2gv.convert(nd, 1,
                                        ctrl_direction=ctrl_direction,
                                        sequence=sequence,
                                        unique=unique)
            text_list.append(text)
        print('\n'.join(text_list))

    def show_tnode_ctext(self, nd_main):
        unique = self.param_yes('unique')
        text_list = []
        for nd in nd_main:
            if not isinstance(nd, LinkNode):
                _plog.warning(f'not a LinkNode {nd.name}')
                continue
            text = self._conv_ln2c.convert(nd, 1,
                                        ctrl_direction = self.param_value('ctrl_direction'),
                                        sequence = self.param_yes('sequence'),
                                        unique=unique)
            text_list.append(text)

        print('\n'.join(text_list))

    def show_tnode_tree(self, nd_main):
        unique = self.param_yes('unique')
        for nd in nd_main:
            if not isinstance(nd, LinkNode):
                _plog.warning(f'{nd}')
                continue

            left, right = nd.left, nd.right

            if nd.jump == 0:
                tnode = SNode(left, '>' if nd.dir not in ('back',) else '<')
                tnode.add_child(right)
                tnode.dump()
                print()
                continue

            tree_dir = '->' if nd.dir not in ('back',) else '<-'
            tnode = self._tree_prov.get_tree(start=left, end=right,
                                             level=nd.jump, direction=tree_dir,
                                             unique=unique)
            if tnode:
                # FIXME:
                if '>' in tree_dir:
                    tnode.dump()
                else:
                    tnode.dump_reverse()
                print()

    def parse_code(self, code):
        if not self._check_param_db():
            print("Please set correct cscope_db path")
            raise mycmd.NewLoop()

        try:
            mode = self.param_value('mode')
            tnode = self._func_parse_code(code)

            if mode in ('c', ):
                self.show_tnode_ctext(tnode)
            elif mode in ('gvtext',):
                self.show_tnode_gvtext(tnode)
            elif mode in ('calltree', 'tree'):
                self.show_tnode_tree(tnode)
            else:
                return False
            return True
        except Exception as e:
            _plog.exception('')
            print(self.err_style(f'{e}'))
            return False

    def default(self, line: str):
        if line.rstrip().endswith(';') and self.parse_code(line):
            return
        return super().default(line)

    def do_ignore(self, line: str):
        '''
        ignore add/del pattern
        ignore list pattern
        '''
        line = replace_blank(line.strip())
        if line.count(' ') == 0:
            print(f'{self.do_ignore.__doc__}')
            return

        cmd, pattern = line.split(maxsplit=1)

        ifilter = self._tree_prov.get_filter()
        if cmd == 'add':
            added = ifilter.add_pattern(pattern, is_ignore=True)
            if added:
                self._tree_prov.set_filter(ifilter)
                print(f'added {pattern}')
            else:
                print(f'{pattern} already exists')
        elif cmd == 'del':
            deleted = ifilter.del_pattern(pattern, is_ignore=True)
            if deleted:
                self._tree_prov.set_filter(ifilter)
                print(f'deleted {pattern}')
            else:
                print(f'{pattern} not exists')
        elif cmd == 'list':
            print('ignore patterns:')
            r = re.compile(pattern)
            for idx, pattern in enumerate(ifilter.patterns_iter(is_ignore=True)):
                if r.match(pattern):
                    print(f'  {idx}: {pattern}')
            print('not ignore patterns:')
            for idx, pattern in enumerate(ifilter.patterns_iter(is_ignore=False)):
                print(f'  {idx}: {pattern}')
        else:
            print(f'{self.do_ignore.__doc__}')

def add_build_subp(subparsers):
    def _handler(args):
        cmd = ''
        if args.uboot or args.kernel:
            cmd += f'cd {args.path} && make cscope'
            if args.arch:
                cmd += f' ALLSOURCE_ARCHS={args.arch}'
        else:
            cmd += f'cd {args.path} && cscope {args.opts}'

        _plog.debug(f'cmd: {cmd}')

        try:
            subprocess.call(cmd, shell=True)
        except Exception as e:
            _plog.error(f'failed: {e}')
            return False
        return True

    subp = subparsers.add_parser('build')

    subp.add_argument('-u', '--u-boot', dest='uboot', action='store_true', help='is u-boot')
    subp.add_argument('-k', '--kernel', action='store_true', help='is kernel')
    subp.add_argument('--arch', type=str, help='arch for u-boot/kernel')
    subp.add_argument('--opts', type=str, default='-Rbqk', help='cscope build options')
    subp.add_argument('path', help='code path')

    subp.set_defaults(func=_handler)

class CTreeCmd(CToolsCmd):
    @classmethod
    def name(cls):
        return "ctree"

    @classmethod
    def description(cls):
        return "cscope tree cli tool"

    def do_command(self, argv:list[str]):
        argp = argparse.ArgumentParser(description='cscope tree cli tool')

        argp.add_argument('-d', '--db', type=str,
                          help="path of cscope.out")
        argp.add_argument('-m', '--mode', type=str,
                          choices=('c', 'gvtext', 'calltree', 'tree',),
                          default='c',
                          help="mode")
        argp.add_argument('-c', '--ctrl_direction', type=str,
                          choices=('>', '<', 'auto'),
                          default='>',
                          help="ctrl direction")

        argp.add_argument('-s', '--tryshell', action="store_true", help="try shell")
        argp.add_argument('-p', '--prompt', type=str, default=_prompt, help="cli prompt")
        argp.add_argument('-r', '--autorep', action="store_true", help="auto repeat")

        subparsers = argp.add_subparsers()

        add_build_subp(subparsers)

        args = argp.parse_args(argv)

        if hasattr(args, 'func'):
            sys.exit(args.func(args))

        if not args.db:
            print('Please set cscope.out path')
            return False

        cli = CTreeCli(args.db,
                        prompt=args.prompt,
                        mode=args.mode, ctrl_direction=args.ctrl_direction,
                        tryshell=args.tryshell, autorep=args.autorep)

        return cli.cmdloop()

if __name__ == '__main__':
    CTreeCmd().do_command(sys.argv[1:])
